package org.blue.restful.client;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.blue.restful.client.annotation.Body;
import org.blue.restful.client.annotation.Header;
import org.blue.restful.client.annotation.Path;
import org.blue.restful.client.annotation.Query;
import org.blue.restful.client.impl.DefaultDeserializer;
import org.blue.restful.client.impl.DefaultSerializer;

public class RestServiceFactory implements InvocationHandler {

	private final String ENCODING = "UTF-8";
	private final int TIMEOUT = 30000;

	URL baseUrl;
	RestSerializer serializer = new DefaultSerializer();
	RestDeserializer deserializer = new DefaultDeserializer();
	
	public RestServiceFactory(URL baseUrl) {
		this.baseUrl = baseUrl;
	}

	public void setSerializer(RestSerializer serializer) {
		this.serializer = serializer;
	}

	public void setDeserializer(RestDeserializer deserializer) {
		this.deserializer = deserializer;
	}

	public <T> T newInstance(Class<T> clazz) {
		@SuppressWarnings("unchecked")
		T result = (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, this);
		return result;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		RestContext ctx = new RestContext(this, proxy, method, args);
		// Returns null if not an annotated HTTP call.
		return !ctx.isValid() ? null : invoke(ctx);
	}

	private Object invoke(RestContext ctx) throws Throwable {

		try {
			URL url = getRequestUrl(ctx);
			System.out.println("URL:" + url);
			HttpURLConnection connection = (HttpURLConnection) url.openConnection();
			connection.setRequestMethod(ctx.requestMethod);
			connection.setDoInput(true);
			connection.setConnectTimeout(TIMEOUT);
			connection.setReadTimeout(TIMEOUT);

			Map<String, String> headers = getRequestHeaders(ctx);
			for (Map.Entry<String, String> entry : headers.entrySet()) {
				connection.setRequestProperty(entry.getKey(), entry.getValue());
				System.out.println("HEADER[" + entry.getKey() + "]:" + entry.getValue());
			}

			String body = getRequestBody(ctx);
			System.out.println("REQ:" + body);
			if (body != null) {
				connection.setDoOutput(true);
				PrintStream out = new PrintStream(connection.getOutputStream());
				out.print(body);
				out.close();
			} else {
				connection.connect();
			}

			int statusCode = connection.getResponseCode();
			System.out.println("RESP_CODE:" + statusCode);

			InputStream input = connection.getInputStream();
			String respStr = IOUtils.toString(input, ENCODING);
			System.out.println(respStr);

			RestDeserializer deserializer = (ctx.deserializer.getClass() == DefaultDeserializer.class) ? this.deserializer
					: ctx.deserializer;
			@SuppressWarnings("unchecked")
			Class<Object> resultClass = (Class<Object>) ctx.resultClass;
			Object result = deserializer.deserialize(resultClass, respStr);
			System.out.println("RESP:" + result);
			return result;
		} catch (IOException e) {
			e.printStackTrace();
			throw e;
		}
	}

	private String getRequestBody(RestContext ctx) throws IOException {
		// Appends query string if necessary.
		Map<String, String> queryArgs = ctx.availableParameters.get(Body.class);
		if (queryArgs != null) {
			StringBuilder sb = new StringBuilder();
			for (Map.Entry<String, String> entry : queryArgs.entrySet()) {
				if (sb.length() != 0)
					sb.append('&');
				sb.append(URLEncoder.encode(entry.getKey(), ENCODING)).append('=')
						.append(URLEncoder.encode(entry.getValue(), ENCODING));
			}
			return sb.toString();
		}
		return null;
	}

	private Map<String, String> getRequestHeaders(RestContext ctx) {
		Map<String, String> headers = ctx.availableParameters.get(Header.class);
		if (headers == null)
			headers = Collections.emptyMap();
		return headers;
	}

	private URL getRequestUrl(RestContext ctx) throws IOException {
		// Replaces path variables.
		String path = ctx.requestPath;
		Map<String, String> pathArgs = ctx.availableParameters.get(Path.class);
		if (pathArgs != null) {
			for (Map.Entry<String, String> entry : pathArgs.entrySet()) {
				path = path.replace('{' + entry.getKey() + '}', entry.getValue() == null ? "" : entry.getValue());
			}
		}
		// Appends query string if required.
		Map<String, String> queryArgs = ctx.availableParameters.get(Query.class);
		if (queryArgs != null) {
			StringBuilder sb = new StringBuilder();
			for (Map.Entry<String, String> entry : queryArgs.entrySet()) {
				if (sb.length() != 0)
					sb.append('&');
				sb.append(URLEncoder.encode(entry.getKey(), ENCODING)).append('=')
						.append(URLEncoder.encode(entry.getValue(), ENCODING));
			}
			path += '?' + sb.toString();
		}

		// The request URL.
		return new URL(this.baseUrl, this.baseUrl.getPath() + path);
	}
}
